Skip to content

feat: [DHIS2-21655] Uncomplete events from view mode - #4649

Draft
henrikmv wants to merge 48 commits into
masterfrom
hv/feat/DHIS2-21655_uncomplete-event-view-mode
Draft

feat: [DHIS2-21655] Uncomplete events from view mode#4649
henrikmv wants to merge 48 commits into
masterfrom
hv/feat/DHIS2-21655_uncomplete-event-view-mode

Conversation

@henrikmv

@henrikmv henrikmv commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

DHIS2-21655

This PR adds the ability to mark events as complete or incomplete directly from view mode
(without entering edit mode). The changes introduce:

  • A new useCanChangeCompletionStatus hook that gates the action on write access,
    event status, and the F_UNCOMPLETE_EVENT authority.

  • A reusable EventCompletionMenuItem component that toggles between ACTIVE
    COMPLETED via the tracker API.

  • Removing duplicate event status lists so the whole app uses one shared source: capture-core/events/statusTypes.

  • Removed the To open this event, please wait until saving is complete tooltip that could get stuck in the Stages and Events widget event row, and repleaced it with the CircularLoader from @dhis2/ui.

@henrikmv henrikmv changed the title feat: [DHIS2-21655] Allow uncompleting expired completed events from view mode feat: [DHIS2-21655] Uncomplete events from view mode Jul 22, 2026
devin-ai-integration[bot]

This comment was marked as resolved.

@henrikmv
henrikmv marked this pull request as ready for review July 22, 2026 11:46
@henrikmv
henrikmv requested a review from a team as a code owner July 22, 2026 11:46

@simonadomnisoru simonadomnisoru left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good!

@github-actions

Copy link
Copy Markdown

@henrikmv

henrikmv commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for review @simonadomnisoru!

I made the changes and also:

  • removed some duplicate event status lists, so the whole app now uses one shared source: capture-core/events/statusTypes.
  • removed the To open this event, please wait until saving is complete tooltip that could get stuck in the Stages and Events widget event row when changing event status, and repleaced it with the CircularLoader from @dhis2/ui.

@henrikmv henrikmv added testing and removed testing labels Aug 5, 2026

@simonadomnisoru simonadomnisoru left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great job! 🥳

@henrikmv
henrikmv marked this pull request as draft August 13, 2026 13:43

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 3 new potential issues.

View 1 additional finding in Devin Review.

Open in Devin Review

Comment on lines +44 to +47
const isCompletionToggleable = (status: string, blockedByCompletion: boolean, blockedByExpiry: boolean) =>
!blockedByCompletion
&& !blockedByExpiry
&& (status === eventStatuses.ACTIVE || status === eventStatuses.COMPLETED);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Users without the uncomplete permission can mark completed events incomplete

The permission to reverse an event's completed status is recomputed from only the expiry and blocking checks (!isEventBlockedByCompletion && !isEventBlockedByExpiry at src/core_modules/capture-core/components/WidgetEventEdit/WidgetEventEdit.container.tsx:120) instead of the authority-aware value, so a user lacking the uncomplete permission is still offered the toggle whenever the stage does not block the form.
Impact: A user who does not have permission to uncomplete events can mark completed events as incomplete (and toggle completion) from the events list, the event widget header, and the edit form, bypassing the intended access control.

How the authority gate is dropped

Previously useEventEditPermissions returned canUncompleteEvent equal to the F_UNCOMPLETE_EVENT authority, and the complete checkbox was disabled for completed events unless the user held that authority. After this PR the hook (src/core_modules/capture-core/hooks/useEventEditPermissions.ts:71-75) computes canUncompleteEvent correctly internally (via canUncompletEvent, which checks hasUncompleteAuthority for completed events) but no longer returns it.

Every consumer now reconstructs the value without the authority check:

  • src/core_modules/capture-core/components/WidgetEventEdit/WidgetEventEdit.container.tsx:120 derives canUncompleteEvent = !isEventBlockedByCompletion && !isEventBlockedByExpiry, passed to WidgetHeader (gating the CompletionMenuItem) and to EditEventDataEntry (gating the complete checkbox disabled state).
  • src/core_modules/capture-core/components/Pages/ViewEvent/EventDetailsSection/EventDetailsSection.component.tsx:92 derives the same value.
  • src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/EventRow.tsx:44-47 (isCompletionToggleable) gates the CompletionMenuItem the same way.

Because isEventBlockedByCompletion = isCompletedAndBlockingForm && !canUncompleteEvent and isCompletedAndBlockingForm is only true when stage.blockEntryForm is set, a COMPLETED event on a stage with blockEntryForm unset (the common case) yields isEventBlockedByCompletion === false and isEventBlockedByExpiry === false, so the derived canUncompleteEvent/canToggleCompletion is true regardless of whether the user holds F_UNCOMPLETE_EVENT.

Prompt for agents
The F_UNCOMPLETE_EVENT authority is no longer enforced when offering the mark-complete/mark-incomplete toggle. useEventEditPermissions (src/core_modules/capture-core/hooks/useEventEditPermissions.ts) internally computes a correct authority-aware canUncompleteEvent (canUncompletEvent, which returns hasUncompleteAuthority for completed events and checks write access), but it no longer returns it. All consumers instead recompute canUncompleteEvent / canToggleCompletion as `!isEventBlockedByCompletion && !isEventBlockedByExpiry`, which omits the F_UNCOMPLETE_EVENT authority check for completed events on stages where blockEntryForm is not set (the common case), and also omits the write-access check. Fix by having the hook return its internal canUncompleteEvent (authority- and write-access-aware) and consuming that value in: WidgetEventEdit.container.tsx:120, EventDetailsSection.component.tsx:92, and EventRow.tsx (isCompletionToggleable / canToggleCompletion). Ensure the EditEventDataEntry complete checkbox and the CompletionMenuItem gating both use the authority-aware value so completed events cannot be uncompleted by users lacking F_UNCOMPLETE_EVENT.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +85 to +91
const { isEventBlockedByCompletion, isEventBlockedByExpiry } = useEventEditPermissions({
programId,
stage: programStage,
eventStatus: eventData?.eventContainer?.event?.status,
occurredAtClient: convertFormToClient(eventData?.dataEntryValues?.occurredAt, dataElementTypes.DATE) as string,
completedAtClient: eventData?.eventContainer?.event?.completedAt,
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 completedAtClient passed as raw server value in some callers

useEventEditPermissions expects completedAtClient in client date format (it feeds isWithinCompleteEventsExpiry, which internally calls convertClientToServer). Several callers convert correctly (e.g. EnrollmentEditEventPage.container.tsx and EventRow.tsx use convertServerToClient(...completedAt...)), but ViewEvent.component.tsx:110 and the new EventDetailsSection.component.tsx:90 pass the raw server completedAt value. WidgetEventEdit.container.tsx:118 does the same. This appears to be a pre-existing inconsistency (the ViewEvent line is unchanged), but the new EventDetailsSection usage propagates it. If the complete-events-expiry window is configured on a program, the expiry calculation for completed events on the view page may be computed from an unconverted date. Worth confirming whether the client/server formats coincide for the DATE type in the active calendar.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +38 to +57
const { event: apiEvent } = await dataEngine.query({
event: {
resource: 'tracker/events',
id: eventId,
params: {
fields: 'event,status,program,programStage,orgUnit,occurredAt,scheduledAt,' +
'enrollment,trackedEntity,attributeOptionCombo,notes,assignedUser,geometry,followUp',
},
},
}) as any;
return dataEngine.mutate({
resource: 'tracker?async=false&importStrategy=UPDATE',
type: 'create',
data: {
events: [{
...apiEvent,
status: newStatus,
}],
},
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Single-event completion mutation omits dataValues from UPDATE payload

CompletionMenuItem fetches the event with a fixed field list that excludes dataValues and then re-submits it via tracker?importStrategy=UPDATE. This mirrors the established bulk-complete pattern (useBulkCompleteEvents.ts, which was also narrowed in this PR to the same field set), so it is presumably safe with the server's merge behavior. However, if the tracker UPDATE strategy treats a missing dataValues array as an intent to clear them, toggling completion from view mode could drop event data values. Recommend confirming server behavior since this now runs for individual events triggered directly by users, not just the pre-existing bulk flow.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants